Nesta análise vamos experimentar diferentes abordagens de poda para o AQE, de forma a evitar que a expansão seja muito longa e acabe por prejudicar as consultas do Elasticsearch.
from itertools import product
import json
import yaml
import pandas as pd
import plotly.express as px
from utils.utils import get_expanded_queries, make_elasticsearch_new_aqe_queries,\
create_new_expanded_queries, create_new_aqe_validation_dataset, create_new_aqe_metrics,\
expanded_with_aqe_boost_order, expanded_with_aqe_order
with open("../conf/config.yaml", "r") as yamlfile:
cfg = yaml.safe_load(yamlfile)
with open("../../dados/regis/regis_queries.json", 'r') as regis_file:
regis_queries = json.load(regis_file)
regis_queries = get_expanded_queries(regis_queries)
regis_queries[:2]
[{'title': 'História da geoquímica na Petrobras',
'query_id': 'Q1',
'expanded_query': '((História da geoquímica na Petrobras) OR ( (historia^1.000 OR história^0.890 OR history^0.571 OR "histórico do campo"^0.525 OR review^0.159 OR revisão^0.164 OR "histórico de caso"^0.225) OR (geoquimica^1.000 OR geoquímica^0.890 OR geoquímicas^0.691 OR geoquímico^0.672 OR geoquimicos^0.736 OR geoquimicas^0.691 OR geoquimico^0.672 OR geochemistry^0.601 OR "geochemical anomaly"^0.225 OR "geochemical interpretation"^0.225 OR "composição dos sedimentos"^0.225 OR "sediment composition"^0.225 OR geology^0.195 OR geologia^0.253 OR petrochemistry^0.153 OR petroquímica^0.184 OR "geochemical cycle"^0.225 OR petrografia^0.255 OR petrography^0.191 OR "análise de rochas"^0.225 OR "rock analysis"^0.225 OR "composição das rochas"^0.225 OR "rock composition"^0.225 OR transect^0.150 OR "geochemical map"^0.225 OR "geochemical exploration"^0.225 OR "geochemical logging"^0.225 OR geophysics^0.179 OR geofísica^0.256 OR "geochemical data"^0.225) OR petrobras^1.000 ))'},
{'title': 'Lógica fuzzy aplicada à industria do petróleo',
'query_id': 'Q2',
'expanded_query': '((Lógica fuzzy aplicada à industria do petróleo) OR ( ("logica fuzzy"^1.000 OR "lógica fuzzy"^0.667 OR "lógica difusa"^0.667 OR "logica nebulosa"^0.667 OR "logica difusa"^0.667 OR "fuzzy logic"^0.667) OR ("industria do petroleo"^1.000 OR "indústria do petróleo"^0.667 OR "industria de petroleo"^0.667 OR "petroleum industry"^0.667) OR (logica^1.000 OR lógica^0.890 OR lógico^0.744) OR fuzzy^1.000 OR (aplicada^1.000 OR aplicado^0.795 OR aplicados^0.756) OR (industria^1.000 OR indústria^0.890 OR industry^0.576) OR (petroleo^1.000 OR petróleo^0.890 OR petróleos^0.705 OR petroleos^0.705 OR petroleum^0.632 OR "gasolina natural"^0.525 OR "natural gas"^0.350 OR "gás natural"^0.423 OR "commingled production"^0.525 OR "produção misturada"^0.525 OR "óleo cru"^0.525 OR "crude oil"^0.353 OR "hydrocarbon potential"^0.225 OR "potencial de hidrocarbonetos"^0.225 OR "fluido do reservatório"^0.225 OR "reservoir fluid"^0.225 OR "raw material"^0.225 OR "matéria prima"^0.224 OR "razão de hidrocarbonetos"^0.225 OR "hydrocarbon ratio"^0.225 OR "fossil fuel"^0.150 OR "combustível fóssil"^0.225) ))'}]
ground_truth = pd.read_csv(
"../../dados/regis/regis_ground_truth.csv"
).rename(
columns={"relevance": "relevance_ground_truth"}
)
ground_truth.head()
| query_id | document_id | relevance_ground_truth | |
|---|---|---|---|
| 0 | Q1 | BR-BG.03944 | 1 |
| 1 | Q1 | BR-BG.03925 | 1 |
| 2 | Q1 | BR-TU.23384 | 0 |
| 3 | Q1 | BR-TU.12209 | 0 |
| 4 | Q1 | BR-BG.04089 | 2 |
Aqui vamos experimentar uma poda com diferentes quantidades de termos utilizando os pesos já estabelecidos pelo AQE.
all_expanded_queries = list()
for query in regis_queries:
new_expanded_queries = create_new_expanded_queries(query["expanded_query"], expansion=expanded_with_aqe_boost_order)
for num_termos, new_expanded_query in new_expanded_queries:
q = query.copy()
q["expanded_query"] = new_expanded_query
q["num_termos"] = num_termos
all_expanded_queries.append(q)
all_expanded_queries[:2]
[{'title': 'História da geoquímica na Petrobras',
'query_id': 'Q1',
'expanded_query': '((História da geoquímica na Petrobras) OR ((historia^0.000 OR história^0.000 OR history^0.000 OR "histórico do campo"^0.000 OR review^0.000 OR revisão^0.000 OR "histórico de caso"^0.000) OR (geoquimica^0.000 OR geoquímica^0.000 OR geoquímicas^0.000 OR geoquímico^0.000 OR geoquimicos^0.000 OR geoquimicas^0.000 OR geoquimico^0.000 OR geochemistry^0.000 OR "geochemical anomaly"^0.000 OR "geochemical interpretation"^0.000 OR "composição dos sedimentos"^0.000 OR "sediment composition"^0.000 OR geology^0.000 OR geologia^0.000 OR petrochemistry^0.000 OR petroquímica^0.000 OR "geochemical cycle"^0.000 OR petrografia^0.000 OR petrography^0.000 OR "análise de rochas"^0.000 OR "rock analysis"^0.000 OR "composição das rochas"^0.000 OR "rock composition"^0.000 OR transect^0.000 OR "geochemical map"^0.000 OR "geochemical exploration"^0.000 OR "geochemical logging"^0.000 OR geophysics^0.000 OR geofísica^0.000 OR "geochemical data"^0.000) OR petrobras^0.000 ))',
'num_termos': 0},
{'title': 'História da geoquímica na Petrobras',
'query_id': 'Q1',
'expanded_query': '((História da geoquímica na Petrobras) OR ((historia^0.100 OR história^0.000 OR history^0.000 OR "histórico do campo"^0.000 OR review^0.000 OR revisão^0.000 OR "histórico de caso"^0.000) OR (geoquimica^0.100 OR geoquímica^0.000 OR geoquímicas^0.000 OR geoquímico^0.000 OR geoquimicos^0.000 OR geoquimicas^0.000 OR geoquimico^0.000 OR geochemistry^0.000 OR "geochemical anomaly"^0.000 OR "geochemical interpretation"^0.000 OR "composição dos sedimentos"^0.000 OR "sediment composition"^0.000 OR geology^0.000 OR geologia^0.000 OR petrochemistry^0.000 OR petroquímica^0.000 OR "geochemical cycle"^0.000 OR petrografia^0.000 OR petrography^0.000 OR "análise de rochas"^0.000 OR "rock analysis"^0.000 OR "composição das rochas"^0.000 OR "rock composition"^0.000 OR transect^0.000 OR "geochemical map"^0.000 OR "geochemical exploration"^0.000 OR "geochemical logging"^0.000 OR geophysics^0.000 OR geofísica^0.000 OR "geochemical data"^0.000) OR petrobras^0.100 ))',
'num_termos': 1}]
Em posse das queries que utilizam diferentes quantidades de termos com boosting do elastic search vamos criar o dataset de validação, o qual possui informações do ground truth da base de dados REGIS.
ranking_result_df = make_elasticsearch_new_aqe_queries(
all_expanded_queries,
cfg,
num_docs=24
)
ranking_result_df.head()
| query_id | num_termos | document_id | relevance_ranking | |
|---|---|---|---|---|
| 0 | Q1 | 0 | BR-BG.03964 | 9.573541 |
| 1 | Q1 | 0 | BR-BG.03967 | 9.460924 |
| 2 | Q1 | 0 | BR-BG.04004 | 9.276192 |
| 3 | Q1 | 0 | BR-TU.20287 | 9.119863 |
| 4 | Q1 | 0 | BR-BT.05005 | 9.103277 |
validation_dataset = create_new_aqe_validation_dataset(ranking_result_df, ground_truth)
validation_dataset.head()
| query_id | num_termos | document_id | relevance_ranking | relevance_ground_truth | evaluated | |
|---|---|---|---|---|---|---|
| 0 | Q1 | 0.0 | BR-BG.03964 | 9.573541 | 2.0 | True |
| 1 | Q1 | 0.0 | BR-BG.03967 | 9.460924 | 3.0 | True |
| 2 | Q1 | 0.0 | BR-BG.04004 | 9.276192 | 1.0 | True |
| 3 | Q1 | 0.0 | BR-TU.20287 | 9.119863 | 0.0 | True |
| 4 | Q1 | 0.0 | BR-BT.05005 | 9.103277 | 1.0 | True |
Agora vamos criar as métricas para cada base de dados e quantidade de termos derivados e visualizar os resultados.
metrics_df = create_new_aqe_metrics(validation_dataset)
metrics_df.head()
| query_id | num_termos | ndcg@24 | ap@24 | eval_prop | |
|---|---|---|---|---|---|
| 0 | Q1 | 0.0 | 0.766699 | 0.355878 | 0.944444 |
| 1 | Q1 | 1.0 | 0.731755 | 0.304273 | 0.886792 |
| 2 | Q1 | 2.0 | 0.732425 | 0.302159 | 0.905660 |
| 3 | Q1 | 3.0 | 0.695701 | 0.290881 | 0.905660 |
| 4 | Q1 | 4.0 | 0.742415 | 0.337229 | 0.927273 |
Vamos agora avaliar as métricas. Vamos utilizar as seguintes métricas:
Vejamos qual a melhor quantidade de termos derivados para cada query:
data_viz = metrics_df.groupby(
"query_id"
).agg({
"ndcg@24": "max"
}).reset_index(
).merge(
metrics_df, how="left", on=["query_id", "ndcg@24"]
).sort_values(
["query_id", "num_termos"]
).drop_duplicates(
subset="query_id", keep="first"
)
data_viz.head()
fig = px.scatter(
data_viz,
x="num_termos",
y="ndcg@24",
labels={
"num_termos": "Número de termos",
"ndcg@24": "NDCG@24",
},
hover_data=["query_id", "num_termos", "ndcg@24"],
title="Melhor número de termos por query",
marginal_x="histogram"
)
fig.show()
Podemos ver que a maior concentração está abaixo dos 5 termos derivados.
Vejamos a média para cada número de termos derivados:
queries_boosts_prod = pd.DataFrame(
product(metrics_df["query_id"].unique(), metrics_df["num_termos"].unique()),
columns=["query_id", "num_termos"]
)
data_viz = queries_boosts_prod.merge(
metrics_df, on=["query_id", "num_termos"], how="left"
).fillna(
method="ffill"
).groupby(
"num_termos"
).agg(
mean_ndcg = ("ndcg@24", "mean")
).reset_index()
fig = px.line(
data_viz,
x="num_termos",
y="mean_ndcg",
labels={
"num_termos": "Número de termos",
"mean_ndcg": "NDCG@24 médio",
},
markers=True,
title="NDCG@24 médio para cada número de termos"
)
fig.show()
Podemos ver que no geral, utilizar 5 termos derivados traz o melhor resultado, o qual é melhor que o Elasticsearch puro.
Vejamos como fica a distribuição dos NDCGs@24 ao utilizar o limiar de poda de 5 termos:
metrics_df_poda = metrics_df.query(
"num_termos <= 5"
).sort_values(
["query_id", "num_termos", "ndcg@24"]
).groupby(
"query_id"
).last()
data_viz = metrics_df.groupby(
"query_id"
).agg({
"ndcg@24": "max"
}).merge(
metrics_df_poda,
on="query_id",
suffixes=(" max", ""),
how="left"
).reset_index(
).melt(
id_vars=["query_id"],
value_vars=["ndcg@24 max", "ndcg@24"],
var_name="metric"
).sort_values(
["metric", "value"], ascending=[True, False]
)
fig = px.bar(
data_viz,
x="query_id",
y="value",
color="metric",
barmode='group',
labels={
"query_id": "Query ID",
"value": "NDCG@24",
},
)
fig.show()
Podemos ver que mais de metade das queries possuem NDCG@24 acima de 0,8. Podemos ver também que apenas quatro das 32 queries tem uma diferença substancial entre o NDCG@24 com cinco termos expandidos e o máximo. São elas: Q19, Q17, Q23 e Q34 .
Vejamos quais as melhores quantidades de termos para essas queries:
queries = ["Q23", "Q34", "Q19", "Q17"]
metrics_df.query(
"query_id.isin(@queries)"
).groupby(
"query_id"
).agg({
"ndcg@24": "max"
}).reset_index(
).merge(
metrics_df, how="left", on=["query_id", "ndcg@24"]
).sort_values(
["query_id", "num_termos"]
).drop_duplicates(
subset="query_id", keep="first"
)
| query_id | ndcg@24 | num_termos | ap@24 | eval_prop | |
|---|---|---|---|---|---|
| 0 | Q17 | 0.560611 | 22.0 | 0.500000 | 0.384615 |
| 7 | Q19 | 0.884441 | 1.0 | 0.666013 | 0.656250 |
| 8 | Q23 | 0.919277 | 2.0 | 0.777877 | 0.818182 |
| 9 | Q34 | 0.784010 | 0.0 | 0.361111 | 0.517241 |
Podemos ver que exceto a Q17 todos os valores foram próximos de 5. A Q17 parece ser um caso atípico de que algum termo que era considerado de baixa relevância trouxe bons resultados.
Aqui vamos experimentar uma poda com diferentes quantidades de termos utilizando a ordem do AQE. A motivação desse experimento é devido ao padrão de fatores de boostings usados, os quais trazem pesos maiores no início e depois vão se misturando, que indica que algum processamento foi realizado.
all_expanded_queries = list()
for query in regis_queries:
new_expanded_queries = create_new_expanded_queries(query["expanded_query"], expansion=expanded_with_aqe_order)
for num_termos, new_expanded_query in new_expanded_queries:
q = query.copy()
q["expanded_query"] = new_expanded_query
q["num_termos"] = num_termos
all_expanded_queries.append(q)
all_expanded_queries[:2]
[{'title': 'História da geoquímica na Petrobras',
'query_id': 'Q1',
'expanded_query': '((História da geoquímica na Petrobras) OR ((historia^0.000 OR história^0.000 OR history^0.000 OR "histórico do campo"^0.000 OR review^0.000 OR revisão^0.000 OR "histórico de caso"^0.000) OR (geoquimica^0.000 OR geoquímica^0.000 OR geoquímicas^0.000 OR geoquímico^0.000 OR geoquimicos^0.000 OR geoquimicas^0.000 OR geoquimico^0.000 OR geochemistry^0.000 OR "geochemical anomaly"^0.000 OR "geochemical interpretation"^0.000 OR "composição dos sedimentos"^0.000 OR "sediment composition"^0.000 OR geology^0.000 OR geologia^0.000 OR petrochemistry^0.000 OR petroquímica^0.000 OR "geochemical cycle"^0.000 OR petrografia^0.000 OR petrography^0.000 OR "análise de rochas"^0.000 OR "rock analysis"^0.000 OR "composição das rochas"^0.000 OR "rock composition"^0.000 OR transect^0.000 OR "geochemical map"^0.000 OR "geochemical exploration"^0.000 OR "geochemical logging"^0.000 OR geophysics^0.000 OR geofísica^0.000 OR "geochemical data"^0.000) OR petrobras^0.000 ))',
'num_termos': 0},
{'title': 'História da geoquímica na Petrobras',
'query_id': 'Q1',
'expanded_query': '((História da geoquímica na Petrobras) OR ((historia^0.100 OR história^0.000 OR history^0.000 OR "histórico do campo"^0.000 OR review^0.000 OR revisão^0.000 OR "histórico de caso"^0.000) OR (geoquimica^0.100 OR geoquímica^0.000 OR geoquímicas^0.000 OR geoquímico^0.000 OR geoquimicos^0.000 OR geoquimicas^0.000 OR geoquimico^0.000 OR geochemistry^0.000 OR "geochemical anomaly"^0.000 OR "geochemical interpretation"^0.000 OR "composição dos sedimentos"^0.000 OR "sediment composition"^0.000 OR geology^0.000 OR geologia^0.000 OR petrochemistry^0.000 OR petroquímica^0.000 OR "geochemical cycle"^0.000 OR petrografia^0.000 OR petrography^0.000 OR "análise de rochas"^0.000 OR "rock analysis"^0.000 OR "composição das rochas"^0.000 OR "rock composition"^0.000 OR transect^0.000 OR "geochemical map"^0.000 OR "geochemical exploration"^0.000 OR "geochemical logging"^0.000 OR geophysics^0.000 OR geofísica^0.000 OR "geochemical data"^0.000) OR petrobras^0.100 ))',
'num_termos': 1}]
Em posse das queries que utilizam diferentes quantidades de termos com boosting do elastic search vamos criar o dataset de validação, o qual possui informações do ground truth da base de dados REGIS.
ranking_result_df = make_elasticsearch_new_aqe_queries(
all_expanded_queries,
cfg,
num_docs=24
)
ranking_result_df.head()
| query_id | num_termos | document_id | relevance_ranking | |
|---|---|---|---|---|
| 0 | Q1 | 0 | BR-BG.03964 | 9.573541 |
| 1 | Q1 | 0 | BR-BG.03967 | 9.460924 |
| 2 | Q1 | 0 | BR-BG.04004 | 9.276192 |
| 3 | Q1 | 0 | BR-TU.20287 | 9.119863 |
| 4 | Q1 | 0 | BR-BT.05005 | 9.103277 |
validation_dataset = create_new_aqe_validation_dataset(ranking_result_df, ground_truth)
validation_dataset.head()
| query_id | num_termos | document_id | relevance_ranking | relevance_ground_truth | evaluated | |
|---|---|---|---|---|---|---|
| 0 | Q1 | 0.0 | BR-BG.03964 | 9.573541 | 2.0 | True |
| 1 | Q1 | 0.0 | BR-BG.03967 | 9.460924 | 3.0 | True |
| 2 | Q1 | 0.0 | BR-BG.04004 | 9.276192 | 1.0 | True |
| 3 | Q1 | 0.0 | BR-TU.20287 | 9.119863 | 0.0 | True |
| 4 | Q1 | 0.0 | BR-BT.05005 | 9.103277 | 1.0 | True |
Agora vamos criar as métricas para cada base de dados e quantidade de termos derivados e visualizar os resultados.
metrics_df = create_new_aqe_metrics(validation_dataset)
metrics_df.head()
| query_id | num_termos | ndcg@24 | ap@24 | eval_prop | |
|---|---|---|---|---|---|
| 0 | Q1 | 0.0 | 0.766699 | 0.355878 | 0.944444 |
| 1 | Q1 | 1.0 | 0.731755 | 0.304273 | 0.886792 |
| 2 | Q1 | 2.0 | 0.732425 | 0.302159 | 0.905660 |
| 3 | Q1 | 3.0 | 0.745183 | 0.337229 | 0.927273 |
| 4 | Q1 | 4.0 | 0.706277 | 0.285714 | 0.928571 |
Vamos agora avaliar as métricas. Vamos utilizar as seguintes métricas:
Vejamos qual a melhor quantidade de termos derivados para cada query:
data_viz = metrics_df.groupby(
"query_id"
).agg({
"ndcg@24": "max"
}).reset_index(
).merge(
metrics_df, how="left", on=["query_id", "ndcg@24"]
).sort_values(
["query_id", "num_termos"]
).drop_duplicates(
subset="query_id", keep="first"
)
data_viz.head()
fig = px.scatter(
data_viz,
x="num_termos",
y="ndcg@24",
labels={
"num_termos": "Número de termos",
"ndcg@24": "NDCG@24",
},
hover_data=["query_id", "num_termos", "ndcg@24"],
title="Melhor número de termos por query",
marginal_x="histogram"
)
fig.show()
Podemos ver que a maior concentração está abaixo dos 5 termos derivados.
Vejamos a média para cada número de termos derivados:
queries_boosts_prod = pd.DataFrame(
product(metrics_df["query_id"].unique(), metrics_df["num_termos"].unique()),
columns=["query_id", "num_termos"]
)
data_viz = queries_boosts_prod.merge(
metrics_df, on=["query_id", "num_termos"], how="left"
).fillna(
method="ffill"
).groupby(
"num_termos"
).agg(
mean_ndcg = ("ndcg@24", "mean")
).reset_index()
fig = px.line(
data_viz,
x="num_termos",
y="mean_ndcg",
labels={
"num_termos": "Número de termos",
"mean_ndcg": "NDCG@24 médio",
},
markers=True,
title="NDCG@24 médio para cada número de termos"
)
fig.show()
Podemos ver que utilizar os primeiros 3 termos derivados traz o melhor resultado, o qual é melhor que o Elasticsearch puro.
Vejamos como fica a distribuição dos NDCGs@24 ao utilizar o limiar de poda de 3 termos:
metrics_df_poda = metrics_df.query(
"num_termos <= 3"
).sort_values(
["query_id", "num_termos", "ndcg@24"]
).groupby(
"query_id"
).last()
data_viz = metrics_df.groupby(
"query_id"
).agg({
"ndcg@24": "max"
}).merge(
metrics_df_poda,
on="query_id",
suffixes=(" max", ""),
how="left"
).reset_index(
).melt(
id_vars=["query_id"],
value_vars=["ndcg@24 max", "ndcg@24"],
var_name="metric"
).sort_values(
["metric", "value"], ascending=[True, False]
)
fig = px.bar(
data_viz,
x="query_id",
y="value",
color="metric",
barmode='group',
labels={
"query_id": "Query ID",
"value": "NDCG@24",
},
)
fig.show()
Podemos ver que metade das queries possuem NDCG@24 acima de 0,8. Podemos ver também que apenas quatro das 32 queries tem uma diferença substancial entre o NDCG@24 com os três primeiros termos expandidos e o máximo. São elas: Q17, Q34, Q19 e Q3.
Vejamos quais as melhores quantidades de termos para essas queries:
queries = ["Q17", "Q34", "Q19", "Q3"]
metrics_df.query(
"query_id.isin(@queries)"
).groupby(
"query_id"
).agg({
"ndcg@24": "max"
}).reset_index(
).merge(
metrics_df, how="left", on=["query_id", "ndcg@24"]
).sort_values(
["query_id", "num_termos"]
).drop_duplicates(
subset="query_id", keep="first"
)
| query_id | ndcg@24 | num_termos | ap@24 | eval_prop | |
|---|---|---|---|---|---|
| 0 | Q17 | 0.828641 | 18.0 | 1.000000 | 0.346154 |
| 10 | Q19 | 0.884441 | 1.0 | 0.666013 | 0.656250 |
| 11 | Q3 | 0.795952 | 17.0 | 0.466080 | 0.615385 |
| 14 | Q34 | 0.784010 | 0.0 | 0.361111 | 0.517241 |
Podemos ver que exceto a Q17 e Q3 todos foram próximos de 3 termos. A Q17 e Q3 parecem ser casos atípicos, em que algum termo que era considerado de baixa relevância trouxe bons resultados.
Nesta análise vimos que a poda dos métodos do AQE ajudam a melhorar as métricas de performance. Vimos duas abordagens de poda: utilizar a ordem dos fatores de boosting e a ordem provinda do AQE. Em ambos, os melhores resultados de NDCG@24 foram na casa de 0.76, sendo ambos superiores ao Elasticsearch puro, mas a ordem do AQE trouxe um resultado ligeiramente melhor. Apesar disso, consideramos que utilizar a ordem dos fatores de boosting é o mecanismo de poda mais confiável, pois a variação do NDCG@24 em maiores número de termos é bem menor, tornando um método de poda mais estável. Além disso, podando com até 5 termos, as queries que trazem maiores diferenças apresentam número de termos mais próximos. Logo, concluímos que utilizar a ordem do fator de boosting com até cinco termos é o método de poda mais razoável para o caso geral.